import logging
from abc import abstractmethod
from datetime import datetime
from time import time
from typing import Tuple

from common.agent_events import AgentEventTag, ExploitationEvent, PropagationEvent
from common.event_queue import IAgentEventPublisher
from common.types import AgentID, Event
from infection_monkey.i_puppet import ExploiterResult, TargetHost

from .zerologon_options import ZerologonOptions
from .zerologon_utils.exceptions import FailedExploitationError

logger = logging.getLogger(__name__)


class HostExploiter:
    @property
    @abstractmethod
    def _EXPLOITED_SERVICE(self):
        pass

    @property
    @abstractmethod
    def _EXPLOITER_TAGS(self) -> Tuple[AgentEventTag, ...]:
        pass

    @property
    @abstractmethod
    def _PROPAGATION_TAGS(self) -> Tuple[AgentEventTag, ...]:
        pass

    def __init__(self):
        self.agent_id = None
        self.exploit_info = {
            "display_name": self._EXPLOITED_SERVICE,
            "started": "",
            "finished": "",
            "vulnerable_urls": [],
            "vulnerable_ports": [],
            "executed_cmds": [],
        }
        self.exploit_attempts = []
        self.host = None
        self.agent_event_publisher = None
        self.options = ZerologonOptions()
        self.exploit_result = {}
        self.servers = []

    def set_start_time(self):
        self.exploit_info["started"] = datetime.now().isoformat()

    def set_finish_time(self):
        self.exploit_info["finished"] = datetime.now().isoformat()

    def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""):
        self.exploit_attempts.append(
            {
                "result": result,
                "user": user,
                "password": password,
                "lm_hash": lm_hash,
                "ntlm_hash": ntlm_hash,
                "ssh_key": ssh_key,
            }
        )

    def exploit_host(
        self,
        agent_id: AgentID,
        host: TargetHost,
        agent_event_publisher: IAgentEventPublisher,
        options: ZerologonOptions,
        interrupt: Event,
    ):
        self.agent_id = agent_id
        self.host = host
        self.agent_event_publisher = agent_event_publisher
        self.options = options
        self.interrupt = interrupt

        self.pre_exploit()
        try:
            return self._exploit_host()
        except FailedExploitationError as e:
            logger.debug(f"Exploiter failed: {e}.")
            raise e
        except Exception as e:
            logger.error("Exception in exploit_host", exc_info=True)
            raise e
        finally:
            self.post_exploit()

    def pre_exploit(self):
        self.exploit_result = ExploiterResult(os=self.host.operating_system, info=self.exploit_info)
        self.set_start_time()

    def _is_interrupted(self):
        return self.interrupt.is_set()

    def post_exploit(self):
        self.set_finish_time()

    @abstractmethod
    def _exploit_host(self) -> ExploiterResult:
        raise NotImplementedError()

    def add_vuln_url(self, url):
        self.exploit_info["vulnerable_urls"].append(url)

    def _publish_exploitation_event(
        self,
        time: float = time(),
        success: bool = False,
        tags: Tuple[AgentEventTag, ...] = tuple(),
        error_message: str = "",
    ):
        exploitation_event = ExploitationEvent(
            source=self.agent_id,
            target=self.host.ip,
            success=success,
            exploiter_name="Zerologon",
            error_message=error_message,
            timestamp=time,
            tags=frozenset(tags or self._EXPLOITER_TAGS),
        )
        self.agent_event_publisher.publish(exploitation_event)

    def _publish_propagation_event(
        self,
        time: float = time(),
        success: bool = False,
        tags: Tuple[AgentEventTag, ...] = tuple(),
        error_message: str = "",
    ):
        propagation_event = PropagationEvent(
            source=self.agent_id,
            target=self.host.ip,
            success=success,
            exploiter_name="Zerologon",
            error_message=error_message,
            timestamp=time,
            tags=frozenset(tags or self._PROPAGATION_TAGS),
        )
        self.agent_event_publisher.publish(propagation_event)
